import os
import socket

from pyparsing import alphas, alphanums, removeQuotes, nums, delimitedList
from pyparsing import pythonStyleComment, cppStyleComment, quotedString, oneOf
from pyparsing import lineno, hexnums, dictOf
from pyparsing import Suppress, Word, Combine, Optional, Keyword, ZeroOrMore
from pyparsing import StringEnd, LineEnd, Literal, ParseFatalException
from pyparsing import ParseException, Forward, Group, OneOrMore, Or, Each, Dict
from pyparsing import LineStart

from venya.lib import stdlib, exception, ip

kword = lambda value: Keyword(value).suppress()
word = lambda value: LineStart().leaveWhitespace() + Keyword(value)

icmp4 = {
        "echo-reply" : 0,
        "echorep" : 0,
        "unreachable" : 3,
        "unreach" : 3,
        "source-quench" : 4,
        "squench" : 4,
        "redirect" : 5,
        "alternate-address" : 6,
        "echo-request" : 8,
        "echo" : 8,
        "echoreq" : 8,
        "router-advertisement" : 9,
        "router-solicitation" : 10,
        "time-exceeded" : 11,
        "timex" : 11,
        "parameter-problem" : 12,
        "timestamp-request" : 13,
        "timestamp-reply" : 14,
        "information-request" : 15,
        "information-reply" : 16,
        "mask-request" : 17,
        "mask-reply" : 18,
        "trace" : 30,
        "traceroute" : 30,
        "conversion-error" :  31,
        "mobile-redirect" : 32,
        "ipv6-where" : 33,
        "ipv6-here" : 34,
        "mobregreq" : 35,
        "mobregrep" : 36,
        "skip" : 39,
        "photuris" : 40,
        }

def get_port(port):
    retval = 0
    if isinstance(port, str):
        if port.isalpha():
            if port == "dns":
                port = "domain"
            elif port == "ike":
                port = "isakmp"
            elif port == "http":
                port = "www"
            elif port == "mail":
                port = "smtp"
            elif port == "dhcp":
                port = "bootpc"

            try:
                retval = socket.getservbyname(port)
            except socket.error as err:
                raise ValueError(err)
        elif port.isdigit():
            port = int(port)
            if port <= 0 or port > 65535:
                raise ValueError("invalid port number")
            else:
                retval = port
    elif isinstance(port, (int, long)):
        if port <= 0 or port > 65535:
            raise ValueError("invalid port number")
        else:
            retval = port

    return retval

def sort_address(pfl):
    retval = {}
    prefix = []
    address = []
    for ix in pfl:
        if isinstance(ix, str):
            prefix.append(ix)
        elif isinstance(ix, ip.ip):
            address.append(ix)

    if len(prefix):
        retval["prefix"] = prefix

    if len(address):
        retval["address"] = address

    return retval

class ConfigFile(object):
    def __init__(self, Filename):
        super(ConfigFile, self).__init__()

        self._file = Filename
        self._header = {}
        self._terms = []
        self._include = []
        self._protocol = []
        self._conffile = []
        self._prefix = []

        self._lb, self._rb = map(Suppress, "{}")
        self._lp, self._rp = map(Suppress, "()")
        self._lbk, self._rbk = map(Suppress, "[]")
        self._lt, self._gt = map(Suppress, "<>")

        self._comma = Literal(",").suppress()
        self._fs = Literal("/").suppress()
        self._eq = Literal("=").suppress()
        self._colon = Literal(":").suppress()

        self._identifier = Word(alphas + alphanums + "-_")
        self._quoted = quotedString.copy().setParseAction(removeQuotes)
        self._integer = Combine(Optional(oneOf("- +")) + Word(nums))
        self._target = oneOf("junos junos-srx")
        self._action = oneOf("accept reject next deny reject-with-tcp-rst")
        self._option = oneOf("established tcp-established sample initial \
                rst first-fragment is-fragment")
        self._comment = Optional(Group(Keyword("comment") + self._eq +
            self._quoted))
        self._ipaddr = Word(nums + "./").setParseAction(self._validateIPAddr)
        self._ip6addr = Word(hexnums + ":/").setParseAction(
                self._validateIPAddr)

    def error(self, fmt, *args):
        errmesg = None
        if len(args):
            errmesg = stdlib.sprintf(fmt, *args)
        else:
            errmesg = str(fmt)

        raise ValueError(errmesg)

    def _addHeader(self, string, location, tokens):
        for key, value in tokens:
            if key == "logical-system":
                self._header["lsys"] = value
            else:
                self._header[key] = value

    def _established(self, string, location, tokens):
        retval = []
        if len(self._protocol) == 1:
            if tokens[0] == "tcp-established" and self._protocol[0] == "tcp":
                retval = ["option", tokens[0]]
            elif tokens[0] == "established" and self._protocol[0] == "udp":
                retval = ["port", "1024-65535"]
            else:
                retval = ["option", tokens[0]]

        return [retval]

    @property
    def termgrammar(self):

        portrange = Group(self._identifier + self._colon +
                self._identifier).setParseAction(self._validatePortRange)
        portoptions = delimitedList(portrange |
                self._identifier).setParseAction(self._validatePort)

        # netaddr = Or(self._ipaddr | self._ip6addr)
        prefixopts = delimitedList(self._ipaddr | self._identifier)
        icmpopts = delimitedList(self._identifier |
                Word(nums)).setParseAction(self._validateIcmp4)

        protocol = Group(Keyword("protocol") + self._eq + delimitedList(
            self._identifier).setParseAction(self._validateProtocol))
        port = Group(Keyword("port") + self._eq + portoptions)
        dstport = Group(Keyword("destination-port") + self._eq + portoptions)
        srcport = Group(Keyword("source-port") + self._eq + portoptions)
        action = Group(Keyword("action") + self._eq + self._action)
        src = Group(Keyword("source") + self._eq + prefixopts)
        dst = Group(Keyword("destination") + self._eq + prefixopts)
        srcex = Group(Keyword("source-exclude") + self._eq + prefixopts)
        dstex = Group(Keyword("destination-exclude") + self._eq + prefixopts)
        icmptypes = Group(Keyword("icmp-type") + self._eq + icmpopts)
        counter = Group(Keyword("counter") + self._eq + self._identifier)
        policer = Group(Keyword("policer") + self._eq + self._identifier)

        termoptions = (
                self._comment &
                action &
                protocol &
                Optional(port | dstport) &
                Optional(srcport) &
                Optional(src) &
                Optional(dst) &
                Optional(srcex) &
                Optional(dstex) &
                Optional(icmptypes) &
                Optional(counter) &
                Optional(policer) &
                Optional(self._option.setParseAction(self._established))
                )

        return (kword("term") + self._identifier +
                self._lb + termoptions + self._rb).setParseAction(
                        self._termHeader)


    def _getAction(self, action):
        retval = None
        if action in ("accept", "permit", "allow"):
            retval = "accept"
        elif action in ("deny", "disallow"):
            retval = "discard"
        else:
            retval = action

        return retval

    def _termHeader(self, string, location, tokens):
        term = {}
        term["name"] = tokens[0]
        term["protocol"] = self._protocol
        for ix in tokens[1:]:
            key = ix[0]
            if key == "comment":
                term["comment"] = ix[1]
            elif key in ("port", "destination-port"):
                term["dstport"] = ix[1:]
            elif key == "source-port":
                term["srcport"] = ix[1:]
            elif key == "action":
                term["action"] = self._getAction(ix[1])
            elif key == "icmp-type":
                term["icmptypes"] = ix[1:]
            elif key in ("source", "destination", "source-exclude",
                    "destination-exclude"):
                retval = sort_address(ix[1:])
                if "prefix" in retval:
                    if key == "source":
                        term["srcprefix"] = retval["prefix"]
                    elif key == "destination":
                        term["dstprefix"] = retval["prefix"]
                    elif key == "source-exclude":
                        term["srcpflex"] = retval["prefix"]
                    elif key == "destination-exclude":
                        term["dstpflex"] = retval["prefix"]

                if "address" in retval:
                    if key == "source":
                        term["srcaddr"] = retval["address"]
                    elif key == "destination":
                        term["dstaddr"] = retval["address"]
                    elif key == "source-exclude":
                        term["srcadex"] = retval["address"]
                    elif key == "destination-exclude":
                        term["dstadex"] = retval["address"]
            elif key == "counter":
                term["counter"] = ix[1]
            elif key == "policer":
                term["policer"] = ix[1]
            elif key == "option":
                term["option"] = ix[1]

        if ("dstport" in term or "srcport" in term) and "protocol" in term:
            if "icmp" in term["protocol"]:
                self.error("port defined for protocol(s) %s in term %s",
                    " ".join(term["protocol"]), tokens[0])

        if "icmptypes" in term and "icmp" not in term["protocol"]:
            self.error("ICMP types defined but protocol is not ICMP in term %s",
                    tokens[0])

        self._terms.append(term)
        self._protocol = []

    def _validateProtocol(self, string, location, tokens):
        for ix in tokens:
            if ix.isalpha():
                try:
                    temp = socket.getprotobyname(ix)
                except:
                    self.error("invalid protocol %s on line %d", ix, lineno(
                        location, string))
            elif ix.isdigit():
                jy = int(ix)
                if jy < 0 or jy > 255:
                    self.error("invalid protocol number %d on line %d", jy,
                            lineno(location, string))

            self._protocol.append(ix)

    def _validatePort(self, string, location, tokens):
        retval = []
        for ix in tokens:
            if isinstance(ix, dict):
                retval.append(ix)
            elif not isinstance(ix, dict):
                try:
                    retval.append(get_port(ix))
                except ValueError as err:
                    self.error("invalid port on line %d", lineno(location,
                        string))

        return retval
    def _validatePortRange(self, string, location, tokens):
        prange = tokens[0]
        retval = {}
        start = end = 0
        try:
            start = get_port(prange[0])
            end = get_port(prange[1])
        except ValueError as err:
            self.error("%s on line %d", str(err), lineno(location, string))

        if start > end:
            self.error("invalid port range on line %d", lineno(location,
                string))
        else:
            retval["start"] = start
            retval["end"] = end

        return retval

    def _validateIPAddr(self, string, location, tokens):
        retval = None
        try:
            retval = ip.ip(tokens[0])
        except exception.IPError as err:
            self.error("invalid ip %s on line %d", tokens[0], lineno(location,
                string))

        return retval

    def _validateIcmp4(self, string, location, tokens):
        icmpval = sorted(set(icmp4.values()))
        icmpkey = sorted(set(icmp4.keys()))
        retval = []
        for ix in tokens:
            if ix.isdigit() and int(ix) in icmpval:
                retval.append(int(ix))
            elif ix.isalpha() and ix in icmpkey:
                retval.append(icmp4[ix])
            else:
                if ix in icmpkey:
                    retval.append(icmp4[ix])
                else:
                    self.error("invalid icmp type %s on line %d", ix, lineno(
                        location, string))

        return retval

    def _test(self, string, location, tokens):
        print(lineno(location, string), tokens)
        print(tokens)

    def _intsp(self, string, location, tokens):
        retval = []
        if tokens[0] == "interface-specific":
            retval = ["intsp", True]

        return [retval]

    def _validateIncludeFile(self, string, location, tokens):
        (option, filename) = tokens
        inclfile = None
        for root, dirs, files in os.walk(os.curdir):
            test = [ix for ix in files]
            if filename in test:
                inclfile = os.path.join(root, filename)
                break


        if not inclfile:
            self.error("%s include file not found", filename)
        else:
            conf = ConfigFile(inclfile)
            self._include.append(conf)

            if option == "terms":
                rule = OneOrMore(self.termgrammar)
                rule.ignore(Literal("\\") + LineEnd())
                rule.ignore(pythonStyleComment)
                rule.ignore(cppStyleComment)
                conf.parse(rule)
            else:
                self.error("unknown option %s in file %s", option, filename)

    def _prefixHeader(self, string, location, tokens):
        pfl = {}
        pfl["name"] = tokens[1]
        pfl["ip"] = tokens[2:]
        self._prefix.append(pfl)

    @property
    def pflgrammar(self):
        prefixopts = delimitedList(self._ipaddr | self._ip6addr)
        prefixlist = (Keyword("prefix-list") + self._identifier +
                self._lb + prefixopts + self._rb).setParseAction(
                        self._prefixHeader)

        return prefixlist

    @property
    def grammar(self):
        target = Group(Keyword("target") + self._eq + self._target)
        name = Group(Keyword("filter") + self._eq + self._identifier)
        lsys = Group(Keyword("logical-system") + self._eq + self._identifier)
        intsp = Keyword("interface-specific").setParseAction(self._intsp)

        headeroptions = (
                self._comment &
                target &
                name &
                Optional(lsys) &
                Optional(intsp)
                ).setParseAction(self._addHeader)
        hdr_g = kword("header") + self._lb + headeroptions + self._rb

        inclopts = oneOf("terms prefix-list")

        include = (kword("include") + inclopts + Word(alphanums + "-._")
                ).setParseAction(self._validateIncludeFile)

        rule = hdr_g + (
                ZeroOrMore(self.pflgrammar) &
                ZeroOrMore(include) &
                OneOrMore(self.termgrammar)) + StringEnd()
        rule.ignore(Literal("\\") + LineEnd())
        rule.ignore(pythonStyleComment)
        rule.ignore(cppStyleComment)

        return rule

    def parse(self, grammar = None):
        rule = self.grammar
        if grammar:
            rule = grammar

        results = None
        try:
            with open(self._file, "r") as fh:
                results = rule.parseString(fh.read())
        except ParseFatalException as err:
            stdlib.error("parsing failed on %s: %s\n", self._file, err)
        except ParseException as err:
            stdlib.error("syntax error on line %d \"%s\" in file %s\n",
                    err.lineno, err.line.strip(), self._file)
        except ValueError as err:
            stdlib.error("%s error: %s\n", self._file, err)

    @property
    def header(self):
        return self._header

    @property
    def terms(self):
        return self._terms

    @property
    def pfls(self):
        return self._prefix

    @property
    def conffile(self):
        return self._conffile

def parse_config(filename):
    conf = ConfigFile(filename)
    conf.parse()
    return conf
